<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ChatGPT Local 100% privado, gratis y maravilloso</title>

  <style>
    body {
      font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
      background: #f0f0f0;
      display: grid;
      place-content: center;
      height: 100dvh;
    }

    main {
      width: 400px;
      max-width: 100%;
      height: 70vh;

      background: #fff;
      border: 1px solid #ccc;
      border-radius: 4px;
      box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
      padding: 8px;
      margin-bottom: 16px;

      overflow-y: auto;
      scroll-behavior: smooth;
    }

    ul {
      display: flex;
      flex-direction: column;
      list-style: none;
      padding: 0;
    }

    .message {
      display: flex;
      flex-direction: column;
      margin: 4px 0;
      padding: 4px 8px;

      span {
        width: 36px;
        height: 36px;
        background: #eee;
        font-size: 12px;
        font-weight: 500;
        display: flex;
        justify-content: center;
        align-items: center;
        border-radius: 999999px;
      }

      p {
        border-radius: 4px;
        padding: 4px 8px;
        margin-top: 4px;
      }

      &.user {
        align-self: flex-end;
        align-items: flex-end;

        span,
        p {
          background: rgb(219, 236, 255);
        }
      }

      &.bot {
        align-self: flex-start;

        span,
        p {
          background: rgb(198, 255, 220);
        }
      }
    }

    form {
      display: flex;

      input {
        border-radius: 999999px;
        flex-grow: 1;
        border: 0;
        padding: 8px;
        margin-right: 8px;
        border: 1px solid #ccc;
      }

      button {
        background: #09f;
        border: 0;
        color: white;
        border-radius: 6px;
        cursor: pointer;
        padding: 8px;
        transition: background .3s ease;

        &[disabled] {
          background: #ccc;
          opacity: .6;
          pointer-events: none;
        }

        &:hover {
          background: rgb(0, 104, 173);
        }
      }
    }

    small {
      font-size: 10px;
      color: #555;
      position: fixed;
      bottom: 10px;
      left: 0;
      right: 0;
      margin: auto;
      width: 400px;
    }

    .loading {
      text-align: center;
      display: flex;
      justify-content: center;
      height: 100%;
      align-items: center;
      flex-direction: column;
      margin-top: 50%;

      i {
        pointer-events: none;
        width: 2.5em;
        height: 2.5em;
        border: 0.4em solid transparent;
        border-color: #eee;
        border-top-color: #3E67EC;
        border-radius: 50%;
        animation: loadingspin 1s linear infinite;
      }

      h4 {
        color: #444;
        margin-bottom: 8px;
      }

      h5 {
        font-weight: 400;
        margin: 0;
        font-size: 10px;
        opacity: .4;
      }
    }

    @keyframes loadingspin {
      100% {
        transform: rotate(360deg)
      }
    }
  </style>

  <script type="module">
    /* 
    en el vídeo usamos "https://esm.run/@mlc-ai/web-llm"
    el problema es que eso siempre es la versión más reciente
    en el código usamos https://cdn.jsdelivr.net/npm/@mlc-ai/web-llm@0.2.46/+esm
    para fijar la versión */
    import { CreateWebWorkerMLCEngine } from "https://esm.run/@mlc-ai/web-llm"

    const $ = el => document.querySelector(el)

    // pongo delante de la variable un símbolo de $
    // para indicar que es un elemento del DOM
    const $form = $('form')
    const $input = $('input')
    const $template = $('#message-template')
    const $messages = $('ul')
    const $container = $('main')
    const $button = $('button')
    const $info = $('small')
    const $loading = $('.loading')

    let messages = []
    let end = false

    const SELECTED_MODEL = 'Llama-3-8B-Instruct-q4f32_1-MLC-1k'

    const engine = await CreateWebWorkerMLCEngine(
      new Worker('./worker.js', { type: 'module' }),
      SELECTED_MODEL,
      {
        initProgressCallback: (info) => {
          $info.textContent = info.text
          if (info.progress === 1 && !end) {
            end = true
            $loading?.parentNode?.removeChild($loading)
            $button.removeAttribute('disabled')
            addMessage("¡Hola! Soy un ChatGPT que se ejecuta completamente en tu navegador. ¿En qué puedo ayudarte hoy?", 'bot')
            $input.focus()
          }
        }
      }
    )

    $form.addEventListener('submit', async (event) => {
      event.preventDefault()
      const messageText = $input.value.trim()

      if (messageText !== '') {
        // añadimos el mensaje en el DOM
        $input.value = ''
      }

      addMessage(messageText, 'user')
      $button.setAttribute('disabled', '')

      const userMessage = {
        role: 'user',
        content: messageText
      }

      messages.push(userMessage)

      const chunks = await engine.chat.completions.create({
        messages,
        stream: true
      })

      let reply = ""

      const $botMessage = addMessage("", 'bot')

      for await (const chunk of chunks) {
        const choice = chunk.choices[0]
        const content = choice?.delta?.content ?? ""
        reply += content
        $botMessage.textContent = reply
      }

      $button.removeAttribute('disabled')
      messages.push({
        role: 'assistant',
        content: reply
      })
      $container.scrollTop = $container.scrollHeight
    })

    function addMessage(text, sender) {
      // clonar el template
      const clonedTemplate = $template.content.cloneNode(true)
      const $newMessage = clonedTemplate.querySelector('.message')

      const $who = $newMessage.querySelector('span')
      const $text = $newMessage.querySelector('p')

      $text.textContent = text
      $who.textContent = sender === 'bot' ? 'GPT' : 'Tú'
      $newMessage.classList.add(sender)

      $messages.appendChild($newMessage)

      $container.scrollTop = $container.scrollHeight

      return $text
    }
  </script>
</head>

<body>
  <main>
    <ul>
      <li class="loading">
        <i></i>
        <h4>Cargando...</h4>
        <h5>Esto puede tardar un poco. Paciencia.</h5>
      </li>
    </ul>
  </main>

  <form>
    <input placeholder="Escribe tu mensaje aquí...">
    <button disabled>Enviar</button>
  </form>

  <small>&nbsp;</small>

  <template id="message-template">
    <li class="message">
      <span></span>
      <p></p>
    </li>
  </template>
</body>

</html>